using System;
using System.IO;
using System.Text;

namespace PHSRAG.Insight21.Common.Filters
{
    /// <summary>
    /// Helper class to manipulate HTML head section in the response before it is sent to the client.
    /// </summary>
    public class HeadFilter : Stream
    {
        #region Member variables

        private readonly Stream _outputStream;
        private readonly StringBuilder _linkTags;
        private readonly StringBuilder _literalText;
        private readonly StringBuilder _metaTags;
        private string _baseHref;
        private string _baseTarget;
        private long _position;
        private string _style;
        private string _title;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="HeadFilter"/> class.
        /// </summary>
        /// <param name="inputStream">The input stream.</param>
        public HeadFilter(Stream inputStream)
        {
            _outputStream = inputStream;
            _linkTags = new StringBuilder();
            _metaTags = new StringBuilder();
            _literalText = new StringBuilder();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the base href.
        /// </summary>
        /// <value>The base href.</value>
        public string BaseHref
        {
            get { return _baseHref; }
            set { _baseHref = value; }
        }

        /// <summary>
        /// Gets or sets the base target.
        /// </summary>
        /// <value>The base target.</value>
        public string BaseTarget
        {
            get { return _baseTarget; }
            set { _baseTarget = value; }
        }

        /// <summary>
        /// Gets or sets the style.
        /// </summary>
        /// <value>The style.</value>
        public string Style
        {
            get { return _style; }
            set { _style = value; }
        }

        /// <summary>
        /// Gets or sets the title.
        /// </summary>
        /// <value>The title.</value>
        public string Title
        {
            get { return _title; }
            set { _title = value; }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Gets the tags.
        /// </summary>
        /// <returns></returns>
        private string GetTags()
        {
            StringBuilder sb = new StringBuilder();

            if (!string.IsNullOrEmpty(Title))
            {
                sb.AppendFormat("<title>{0}</title>", Title);
            }
            if (!string.IsNullOrEmpty(BaseHref))
            {
                sb.AppendFormat("<base href={0}{1}{0}>", (char) 34, BaseHref);
            }
            if (!string.IsNullOrEmpty(BaseTarget))
            {
                sb.AppendFormat("<base target={0}{1}{0}>", (char) 34, BaseTarget);
            }
            if (!string.IsNullOrEmpty(Style))
            {
                sb.AppendFormat("<style type={0}text/css{0}>{1}</style>", (char) 34, Style);
            }
            if (!string.IsNullOrEmpty(_linkTags.ToString()))
            {
                sb.Append(_linkTags.ToString());
            }
            if (!string.IsNullOrEmpty(_metaTags.ToString()))
            {
                sb.Append(_metaTags.ToString());
            }
            if (!string.IsNullOrEmpty(_literalText.ToString()))
            {
                sb.Append(_literalText.ToString());
            }

            return sb.ToString();
        }

        /// <summary>
        /// Appends text to text between strings.
        /// </summary>
        /// <param name="originalText">The original text.</param>
        /// <param name="firstBorderingString">The first bordering string.</param>
        /// <param name="secondBorderingString">The second bordering string.</param>
        /// <param name="textToAppend">The text to append.</param>
        /// <returns></returns>
        private static string AppendTextBetweenStrings(string originalText, string firstBorderingString, string secondBorderingString, string textToAppend)
        {
            int target1Position = originalText.IndexOf(firstBorderingString, StringComparison.Ordinal);
            if (target1Position == -1)
            {
                // cannot find the bordering string, simply return the original text
                return originalText;
            }

            int target2Position = originalText.IndexOf(secondBorderingString, StringComparison.Ordinal);
            if (target2Position == -1)
            {
                // cannot find the bordering string, simply return the original text
                return originalText;
            }

            int target1Length = firstBorderingString.Length;

            // found the bordering strings
            string middleText = originalText.Substring(target1Position + target1Length, target2Position - target1Position - target1Length);
            if (!string.IsNullOrEmpty(middleText))
            {
                // must append to the existing middle text
                return originalText.Replace(middleText, middleText + textToAppend);
            }

            // there is nothing between the bordering strings, must insert the new text between the bordering strings
            string firstChunk = originalText.Substring(1, target1Position + target1Length - 1);
            string lastChunk = originalText.Substring(target2Position);
            return firstChunk + textToAppend + lastChunk;
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Adds the link tag.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="relationship">The relationship.</param>
        /// <param name="href">The href.</param>
        public void AddLinkTag(string type, string relationship, string href)
        {
            _linkTags.AppendFormat("<link type={0}{1}{0} rel={0}{2}{0} href={0}{3}{0}>", (char) 34, type, relationship, href);
        }

        /// <summary>
        /// Adds the link tag.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="relationship">The relationship.</param>
        /// <param name="href">The href.</param>
        /// <param name="linkCharacterSet">The link character set.</param>
        /// <param name="hrefLang">The href lang.</param>
        /// <param name="media">The media.</param>
        /// <param name="reverseRelationship">The reverse relationship.</param>
        /// <param name="target">The target.</param>
        public void AddLinkTag(string type, string relationship, string href, string linkCharacterSet, string hrefLang, string media, string reverseRelationship, string target)
        {
            _linkTags.AppendFormat("<link type={0}{1}{0} rel={0}{2}{0} href={0}{3}{0} charset={0}{4}{0} hreflang={0}{5}{0} media={0}{6}{0} rev={0}{7}{0} target={0}{8}{0}>", (char) 34, type, relationship, href, linkCharacterSet, hrefLang, media, reverseRelationship, target);
        }

        /// <summary>
        /// Adds the literal text.
        /// </summary>
        /// <param name="literalText">The literal text.</param>
        public void AddLiteralText(string literalText)
        {
            _literalText.Append(literalText);
        }

        /// <summary>
        /// Adds the meta tag.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="httpEquiv">The HTTP equiv.</param>
        /// <param name="content">The content.</param>
        /// <param name="scheme">The scheme.</param>
        public void AddMetaTag(string name, string httpEquiv, string content, string scheme)
        {
            // should only have Name or HTTPEquiv, not both
            if (!string.IsNullOrEmpty(name))
            {
                _metaTags.AppendFormat("<meta name={0}{1}{0} content={0}{2}{0} scheme={0}{3}{0}>", (char) 34, name, content, scheme);
            }
            else
            {
                _metaTags.AppendFormat("<meta http-equiv={0}{1}{0} content={0}{2}{0} scheme={0}{3}{0}>", (char) 34, httpEquiv, content, scheme);
            }
        }

        /// <summary>
        /// Adds the style sheet reference.
        /// </summary>
        /// <param name="styleSheetPath">The style sheet path.</param>
        public void AddStyleSheetReference(string styleSheetPath)
        {
            AddLinkTag("text/css", "stylesheet", styleSheetPath);
        }

        #endregion

        #region Overrides

        /// <summary>
        /// When overridden in a derived class, gets a value indicating whether the current stream supports reading.
        /// </summary>
        /// <value></value>
        /// <returns>true if the stream supports reading; otherwise, false.</returns>
        public override bool CanRead
        {
            get { return true; }
        }

        /// <summary>
        /// When overridden in a derived class, gets a value indicating whether the current stream supports seeking.
        /// </summary>
        /// <value></value>
        /// <returns>true if the stream supports seeking; otherwise, false.</returns>
        public override bool CanSeek
        {
            get { return true; }
        }

        /// <summary>
        /// When overridden in a derived class, gets a value indicating whether the current stream supports writing.
        /// </summary>
        /// <value></value>
        /// <returns>true if the stream supports writing; otherwise, false.</returns>
        public override bool CanWrite
        {
            get { return true; }
        }

        /// <summary>
        /// When overridden in a derived class, gets the length in bytes of the stream.
        /// </summary>
        /// <value></value>
        /// <returns>A long value representing the length of the stream in bytes.</returns>
        /// <exception cref="T:System.NotSupportedException">A class derived from Stream does not support seeking. </exception>
        /// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
        public override long Length
        {
            get { return _outputStream.Length; }
        }

        /// <summary>
        /// When overridden in a derived class, gets or sets the position within the current stream.
        /// </summary>
        /// <value></value>
        /// <returns>The current position within the stream.</returns>
        /// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
        /// <exception cref="T:System.NotSupportedException">The stream does not support seeking. </exception>
        /// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
        public override long Position
        {
            get { return _position; }
            set { _position = value; }
        }

        /// <summary>
        /// Writes the specified my buffer.
        /// </summary>
        /// <param name="buffer">My buffer.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="count">The count.</param>
        public override void Write(byte[] buffer, int offset, int count)
        {
            string bufferString = Encoding.UTF8.GetString(buffer, offset, count);
            bufferString = bufferString.Replace("</head>", GetTags() + "</head>");
            byte[] output = Encoding.UTF8.GetBytes(bufferString);
            
            _outputStream.Write(output, 0, output.Length);
        }

        /// <summary>
        /// Closes the current stream and releases any resources (such as sockets and file handles) associated with the current stream.
        /// </summary>
        public override void Close()
        {
            _outputStream.Close();
        }

        /// <summary>
        /// When overridden in a derived class, clears all buffers for this stream and causes any buffered data to be written to the underlying device.
        /// </summary>
        /// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
        public override void Flush()
        {
            _outputStream.Flush();
        }

        /// <summary>
        /// Reads the specified my buffer.
        /// </summary>
        /// <param name="buffer">The buffer.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="count">The count.</param>
        /// <returns></returns>
        public override int Read(byte[] buffer, int offset, int count)
        {
            return _outputStream.Read(buffer, offset, count);
        }

        /// <summary>
        /// Seeks the specified offset.
        /// </summary>
        /// <param name="offset">The offset.</param>
        /// <param name="direction">The direction.</param>
        /// <returns></returns>
        public override long Seek(long offset, SeekOrigin direction)
        {
            return _outputStream.Seek(offset, direction);
        }

        /// <summary>
        /// Sets the length.
        /// </summary>
        /// <param name="length">The length.</param>
        public override void SetLength(long length)
        {
            _outputStream.SetLength(length);
        }

        #endregion
    }
}